/*
 * Copyright (C) 2010 TopCoder Inc., All Rights Reserved.
 */
package com.hercules.xre.protocol.testing.handlers;

import com.hercules.xre.protocol.testing.EventHandlerException;
import com.hercules.xre.protocol.testing.handlers.imageserializer.ImageIOImageSerializer;
import com.hercules.xre.protocol.testing.handlers.screenshotcapturer.JavaRobotScreenshotCapturer;

import java.awt.image.BufferedImage;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;

/**
 * This class represents screenshot event handler. It provides function to capture screenshot of active window, save it
 * to file and optionally compare it with the reference screenshot. It extends BaseEventHandler to provide logging.
 *
 * <pre>
 * // create instance using default constructor
 * ScreenshotEventHandler instance = new ScreenshotEventHandler();
 *
 * // if we need logging, we can set logger by name, directly
 * instance.setLoggerName(&quot;test&quot;);
 * instance.setLogger(LogManager.getLog(&quot;test&quot;));
 *
 * // now we need to set file name, where screenshot will be saved
 * // it is SimpleDateFormat compatible, so you can use something like yyyy-MM-dd'.bmp'
 * instance.setImageOutputFilename(&quot;'test_files/screenshot.jpg'&quot;);
 *
 * // the other properties have their defaults, so setting them is not required
 * instance.setImageOutputFormat(&quot;jpg&quot;);
 * instance.setImageSerializer(new ImageIOImageSerializer());
 * instance.setScreenshotCapturer(new JavaRobotScreenshotCapturer());
 *
 * // if we set next two values, then screenshot will compared to referenced screenshot
 * instance.setImageComparer(new PixelBasedImageComparer());
 * instance.setReferenceScreenshot(loadImage(&quot;image.bmp&quot;));
 *
 * // now we can call handleConnectionXXX methods
 * // this methods will capture screenshot, save it to file and optionally compare
 * try {
 *     instance.handleConnectionCreated(&quot;event&quot;);
 * } catch (EventHandlerException e) {
 *     System.out.print(&quot;Captured screenshot is not equal to referenced screenshot.&quot;);
 * }
 *
 * try {
 *     // now we can turn off comparison to just capture screenshots
 *     instance.setImageComparer(null);
 *     instance.setReferenceScreenshot(null);
 *
 *     // call other methods
 *     instance.handleConnectionEvent(&quot;guid&quot;, &quot;event&quot;);
 *     instance.handleConnectionDisconnected(&quot;guid&quot;, new ArrayList&lt;String&gt;());
 *     instance.handleConnectionShutdown(&quot;guid&quot;, &quot;event&quot;);
 * } catch (EventHandlerException e) {
 *     // handle the error
 *     e.printStackTrace();
 * }
 * </pre>
 * <p>
 * <b>Thread safety:</b> This class is effectively thread safe (see restriction below). It synchronizes on the methods
 * that would write to file. This class will be thread safe if getter and setter are be called before calling any other
 * method.
 *
 * @author bramandia, 7realm
 * @version 1.0
 */
public class ScreenshotEventHandler extends BaseEventHandler {
    /**
     * Represents the ImageComparer to be used to compare image. Initialized to null. Has getter/setter. Can be null to
     * indicate no comparison is to be performed. It is used in all handleXXX methods.
     */
    private ImageComparer imageComparer;
    /**
     * Represents the reference screenshot. Initialized to null. Has getter/setter. It is used in handleEvent method. If
     * no comparison is to be performed, this can be null. Otherwise, it must not be null.
     */
    private BufferedImage referenceScreenshot;
    /**
     * Represents the ScreenshotCapturer to be used to capture screenshot. Has getter/setter. Must not be null after
     * initialization. It is used in handleEvent method.
     */
    private ScreenshotCapturer screenshotCapturer = new JavaRobotScreenshotCapturer();
    /**
     * Represents the image format when the screen shot image is saved into the imageOuputFileName. Initialized to
     * "png". Has getter/setter. Must not be null/empty after initialization. It is used in handleEvent method. Valid
     * value: "png", "jpg", "bmp", etc.
     */
    private String imageOutputFormat = "png";
    /**
     * Represents the filename where the screen shot image would be written to. Initialized to null. Has getter/setter.
     * Must not be null/empty after initialization. It is used in handleEvent method. This can supports some template
     * that is compatible with SimpleDateFormat pattern.
     */
    private String imageOutputFilename;
    /**
     * Represents the ImageSerializer to be used to serialize image to output file. Has getter/setter. Must not be null
     * after initialization. It is used in handleEvent method.
     */
    private ImageSerializer imageSerializer = new ImageIOImageSerializer();

    /**
     * Empty default constructor.
     */
    public ScreenshotEventHandler() {
        // empty
    }

    /**
     * Gets the ImageComparer to be used to compare image.
     *
     * @return the ImageComparer to be used to compare image
     */
    public ImageComparer getImageComparer() {
        String methodName = "getImageComparer";
        Helper.logEnter(getLogger(), methodName);
        return Helper.logExit(getLogger(), methodName, imageComparer);
    }

    /**
     * Sets the ImageComparer to be used to compare image.
     *
     * @param imageComparer the new value of the ImageComparer to be used to compare image
     */
    public void setImageComparer(ImageComparer imageComparer) {
        String methodName = "setImageComparer";
        Helper.logEnter(getLogger(), methodName, "imageComparer", imageComparer);
        this.imageComparer = imageComparer;
        Helper.logExit(getLogger(), methodName);
    }

    /**
     * Gets the reference screenshot.
     *
     * @return the reference screenshot
     */
    public BufferedImage getReferenceScreenshot() {
        String methodName = "getReferenceScreenshot";
        Helper.logEnter(getLogger(), methodName);
        return Helper.logExit(getLogger(), methodName, referenceScreenshot);
    }

    /**
     * Sets the reference screenshot.
     *
     * @param referenceScreenshot the new value of the reference screenshot
     */
    public void setReferenceScreenshot(BufferedImage referenceScreenshot) {
        String methodName = "setReferenceScreenshot";
        Helper.logEnter(getLogger(), methodName, "referenceScreenshot", referenceScreenshot);
        this.referenceScreenshot = referenceScreenshot;
        Helper.logExit(getLogger(), methodName);
    }

    /**
     * Gets the ScreenshotCapturer to be used to capture screenshot.
     *
     * @return the ScreenshotCapturer to be used to capture screenshot
     */
    public ScreenshotCapturer getScreenshotCapturer() {
        String methodName = "getScreenshotCapturer";
        Helper.logEnter(getLogger(), methodName);
        return Helper.logExit(getLogger(), methodName, screenshotCapturer);
    }

    /**
     * Sets the ScreenshotCapturer to be used to capture screenshot.
     *
     * @param screenshotCapturer the new value of the ScreenshotCapturer to be used to capture screenshot
     * @throws IllegalArgumentException if argument is null
     */
    public void setScreenshotCapturer(ScreenshotCapturer screenshotCapturer) {
        String methodName = "setScreenshotCapturer";

        try {
            // log enter and check argument
            Helper.logEnterAndCheck(getLogger(), methodName, "screenshotCapturer", screenshotCapturer);
            this.screenshotCapturer = screenshotCapturer;

            Helper.logExit(getLogger(), methodName);
        } catch (IllegalArgumentException e) {
            throw Helper.logIAE(getLogger(), e);
        }
    }

    /**
     * Gets the image format when the screen shot image is saved into the imageOuputFileName.
     *
     * @return the image format when the screen shot image is saved into the imageOuputFileName
     */
    public String getImageOutputFormat() {
        String methodName = "getImageOutputFormat";
        Helper.logEnter(getLogger(), methodName);
        return Helper.logExit(getLogger(), methodName, imageOutputFormat);
    }

    /**
     * Sets the image format when the screen shot image is saved into the imageOuputFileName.
     *
     * @param imageOutputFormat the new value of the image format when the screen shot image is saved into the
     * imageOuputFileName
     * @throws IllegalArgumentException if argument is null/empty
     */
    public void setImageOutputFormat(String imageOutputFormat) {
        String methodName = "setImageOutputFormat";

        try {
            // log enter and check argument
            Helper.logEnterAndCheck(getLogger(), methodName, "imageOutputFormat", imageOutputFormat);
            this.imageOutputFormat = imageOutputFormat;

            Helper.logExit(getLogger(), methodName);
        } catch (IllegalArgumentException e) {
            throw Helper.logIAE(getLogger(), e);
        }
    }

    /**
     * Gets the filename where the screen shot image would be written to.
     *
     * @return the filename where the screen shot image would be written to
     */
    public String getImageOutputFilename() {
        String methodName = "getImageOutputFilename";
        Helper.logEnter(getLogger(), methodName);
        return Helper.logExit(getLogger(), methodName, imageOutputFilename);
    }

    /**
     * Sets the filename where the screen shot image would be written to.
     *
     * @param imageOutputFilename the new value of the filename where the screen shot image would be written to
     * @throws IllegalArgumentException if argument is null/empty or format is not compatible with SimpleDateFormat
     */
    public void setImageOutputFilename(String imageOutputFilename) {
        String methodName = "setImageOutputFilename";

        try {
            // log enter and check argument
            Helper.logEnterAndCheck(getLogger(), methodName, "imageOutputFilename", imageOutputFilename);
            try {
                // check compatibility with SimpleDateFormat
                // we cannot store this instance, because SimpleDateFormat is thread unsafe
                new SimpleDateFormat(imageOutputFilename);
            } catch (IllegalArgumentException e) {
                throw new IllegalArgumentException("Image file name '" + imageOutputFilename
                    + "' is not compatible with SimpleDateFormat.", e);
            }
            this.imageOutputFilename = imageOutputFilename;

            Helper.logExit(getLogger(), methodName);
        } catch (IllegalArgumentException e) {
            throw Helper.logIAE(getLogger(), e);
        }
    }

    /**
     * Gets the ImageSerializer to be used to serialize image to output file.
     *
     * @return the ImageSerializer to be used to serialize image to output file
     */
    public ImageSerializer getImageSerializer() {
        String methodName = "getImageSerializer";
        Helper.logEnter(getLogger(), methodName);
        return Helper.logExit(getLogger(), methodName, imageSerializer);
    }

    /**
     * Sets the ImageSerializer to be used to serialize image to output file.
     *
     * @param imageSerializer the new value of the ImageSerializer to be used to serialize image to output file
     * @throws IllegalArgumentException if argument is null
     */
    public void setImageSerializer(ImageSerializer imageSerializer) {
        String methodName = "setImageSerializer";

        try {
            // log enter and check argument
            Helper.logEnterAndCheck(getLogger(), methodName, "imageSerializer", imageSerializer);
            this.imageSerializer = imageSerializer;

            Helper.logExit(getLogger(), methodName);
        } catch (IllegalArgumentException e) {
            throw Helper.logIAE(getLogger(), e);
        }
    }

    /**
     * Handle the connection created.
     *
     * @param event the event to be handled, must not be null/empty
     * @throws IllegalArgumentException if any argument is invalid
     * @throws IllegalStateException if imageOutputFilename is not set or imageComparer and referenceScreenshot are not
     * simultaneously null or not null
     * @throws ScreenshotDifferException if the screen shot differs on comparison
     * @throws EventHandlerException if any error occurred (wrap any other exception)
     */
    public void handleConnectionCreated(String event) throws EventHandlerException {
        handleEvent("handleConnectionCreated", "event", event);
    }

    /**
     * Handle the connection event.
     *
     * @param sessionGUID the unique session GUID for the application, must not be null/empty
     * @param event the event to be handled, must not be null/empty
     * @throws IllegalArgumentException if any argument is invalid
     * @throws IllegalStateException if imageOutputFilename is not set or imageComparer and referenceScreenshot are not
     * simultaneously null or not null
     * @throws ScreenshotDifferException if the screen shot differs on comparison
     * @throws EventHandlerException if any error occurred (wrap any other exception)
     */
    public void handleConnectionEvent(String sessionGUID, String event) throws EventHandlerException {
        handleEvent("handleConnectionEvent", "sessionGUID", sessionGUID, "event", event);
    }

    /**
     * Handle the connection disconnected.
     *
     * @param sessionGUID the unique session GUID for the application, must not be null/empty
     * @param unsentCommands the list of unsentCommand strings to be handled, must not be null nor contain null/empty
     * element
     * @throws IllegalArgumentException if any argument is invalid
     * @throws IllegalStateException if imageOutputFilename is not set or imageComparer and referenceScreenshot are not
     * simultaneously null or not null
     * @throws ScreenshotDifferException if the screen shot differs on comparison
     * @throws EventHandlerException if any error occurred (wrap any other exception)
     */
    public void handleConnectionDisconnected(String sessionGUID, List<String> unsentCommands)
        throws EventHandlerException {
        handleEvent("handleConnectionDisconnected", "sessionGUID", sessionGUID, "unsentCommands", unsentCommands);
    }

    /**
     * Handle the connection shutdown.
     *
     * @param sessionGUID the unique session GUID for the application, must not be null/empty
     * @param event the event to be handled, must not be null/empty
     * @throws IllegalArgumentException if any argument is invalid
     * @throws IllegalStateException if imageOutputFilename is not set or imageComparer and referenceScreenshot are not
     * simultaneously null or not null
     * @throws ScreenshotDifferException if the screen shot differs on comparison
     * @throws EventHandlerException if any error occurred (wrap any other exception)
     */
    public void handleConnectionShutdown(String sessionGUID, String event) throws EventHandlerException {
        handleEvent("handleConnectionShutdown", "sessionGUID", sessionGUID, "event", event);
    }

    /**
     * Handle the event. Perform screen capture, save to output, optionally perform comparison.
     *
     * @param methodName the name of calling method
     * @param params the parameters of calling method
     * @throws IllegalArgumentException if arguments are incorrect
     * @throws IllegalStateException if imageOutputFilename is not set or imageComparer and referenceScreenshot are not
     * simultaneously null or not null
     * @throws ScreenshotDifferException if the screen shot differs on comparison
     * @throws EventHandlerException if any error occurred (wrap any other exception)
     */
    private void handleEvent(String methodName, Object... params) throws EventHandlerException {
        try {
            // log enter and check argument
            Helper.logEnterAndCheck(getLogger(), methodName, params);

            // check state
            Helper.checkState(imageOutputFilename != null, "The image output filename is not set.");
            Helper.checkState(!(imageComparer == null ^ referenceScreenshot == null),
                "The imageComparer and referenceScreenshot should simultaneously be null or not null.");

            try {
                // get screenshot and serialize it
                String formattedFilename = applyTemplate(imageOutputFilename);
                BufferedImage image = screenshotCapturer.captureActiveWindowScreenshot();
                imageSerializer.serializeImage(image, imageOutputFormat, formattedFilename);
                Helper.log(getLogger(), "Screenshot was serialized to file '" + formattedFilename + "'.");

                // compare screenshots
                if (imageComparer != null) {
                    if (imageComparer.compare(image, referenceScreenshot)) {
                        Helper.log(getLogger(),
                            "Current screenshot was successfully compared with reference screenshot.");
                    } else {
                        throw new ScreenshotDifferException("Captured image is not equal to referenced image. File: '"
                            + imageOutputFilename + "'.");
                    }
                }

                Helper.logExit(getLogger(), methodName);
            } catch (ImageComparerException e) {
                throw new EventHandlerException("Error occurred while comparing images.", e);
            } catch (ImageSerializerException e) {
                throw new EventHandlerException("Error occurred while serializing image to file.", e);
            } catch (ScreenshotCapturerException e) {
                throw new EventHandlerException("Error occurred while capturing screenshot.", e);
            }
        } catch (IllegalArgumentException e) {
            throw Helper.logIAE(getLogger(), e);
        } catch (IllegalStateException e) {
            throw Helper.logISE(getLogger(), e);
        } catch (EventHandlerException e) {
            throw Helper.logError(getLogger(), "Error occurred while handling connection event.", e);
        }
    }

    /**
     * Apply the template in the input string and return the result.
     *
     * @param imageOutputFilename the input file format, must be SimpleDateFormat compatible
     * @return the applied template on input
     */
    private static String applyTemplate(String imageOutputFilename) {
        return new SimpleDateFormat(imageOutputFilename).format(new Date());
    }
}
